]> git.r.bdr.sh - rbdr/map/blame - Map/Presentation/Base Components/MapTextEditor.swift
Add license notices
[rbdr/map] / Map / Presentation / Base Components / MapTextEditor.swift
CommitLineData
98f09799
RBR
1/*
2 Copyright (C) 2024 Rubén Beltrán del Río
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see https://map.tranquil.systems.
16 */
5e8ff485
RBR
17import Cocoa
18import SwiftUI
19
20class MapTextEditorController: NSViewController {
21
e2c37ac1 22 @Binding var document: MapDocument
fdb4633d 23 let onChange: () -> Void
5e8ff485 24
77d0155b
RBR
25 private let vertexRegex = MapParsingPatterns.vertex
26 private let edgeRegex = MapParsingPatterns.edge
27 private let blockerRegex = MapParsingPatterns.blocker
28 private let opportunityRegex = MapParsingPatterns.opportunity
fdb4633d 29 private let noteRegex = MapParsingPatterns.note
77d0155b 30 private let stageRegex = MapParsingPatterns.stage
e2c37ac1 31 private let groupRegex = MapParsingPatterns.group
77d0155b 32
fdb4633d 33 private let changeDebouncer: Debouncer = Debouncer(seconds: 1)
77d0155b 34
e2c37ac1
RBR
35 init(document: Binding<MapDocument>, onChange: @escaping () -> Void) {
36 self._document = document
fdb4633d 37 self.onChange = onChange
5e8ff485
RBR
38 super.init(nibName: nil, bundle: nil)
39 }
40
41 required init?(coder: NSCoder) {
42 fatalError("init(coder:) has not been implemented")
43 }
44
45 override func loadView() {
46 let scrollView = NSTextView.scrollableTextView()
47 let textView = scrollView.documentView as! NSTextView
48
49 scrollView.translatesAutoresizingMaskIntoConstraints = false
50
e2c37ac1 51 textView.backgroundColor = .ui.background
75a0e450 52 textView.allowsUndo = true
5e8ff485 53 textView.delegate = self
77d0155b 54 textView.textStorage?.delegate = self
e2c37ac1 55 textView.string = self.document.text
5e8ff485
RBR
56 textView.isEditable = true
57 textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular)
58 self.view = scrollView
59 }
60
61 override func viewDidAppear() {
62 self.view.window?.makeFirstResponder(self.view)
63 }
64}
65
66extension MapTextEditorController: NSTextViewDelegate {
67
68 func textDidChange(_ obj: Notification) {
69 if let textField = obj.object as? NSTextView {
e2c37ac1
RBR
70 self.document.text = textField.string
71
72 changeDebouncer.debounce {
73 DispatchQueue.main.async {
74 self.onChange()
fdb4633d 75 }
e2c37ac1 76 }
5e8ff485
RBR
77 }
78 }
79
80 func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool
81 {
82 let range = Range(shouldChangeTextIn, in: view.string)
83 let target = view.string[range!]
84
85 if target == "--" {
86 return false
87 }
88
89 return true
90 }
91}
92
77d0155b 93extension MapTextEditorController: NSTextStorageDelegate {
fdb4633d 94
77d0155b
RBR
95 override func textStorageDidProcessEditing(_ obj: Notification) {
96 if let textStorage = obj.object as? NSTextStorage {
fdb4633d 97 self.colorizeText(textStorage: textStorage)
77d0155b
RBR
98 }
99 }
100
101 private func colorizeText(textStorage: NSTextStorage) {
102 let range = NSMakeRange(0, textStorage.length)
103 var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range)
77d0155b
RBR
104
105 for match in matches {
e2c37ac1
RBR
106 textStorage.addAttributes(
107 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1))
108 textStorage.addAttributes(
109 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
110 textStorage.addAttributes(
111 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 3))
112 textStorage.addAttributes(
113 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 4))
77d0155b
RBR
114 }
115
116 matches = edgeRegex.matches(in: textStorage.string, options: [], range: range)
117
118 for match in matches {
e2c37ac1
RBR
119 textStorage.addAttributes(
120 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1))
77d0155b
RBR
121 let arrowRange = match.range(at: 2)
122 textStorage.addAttributes(
fdb4633d 123 [.foregroundColor: NSColor.syntax.symbol],
77d0155b 124 range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1))
e2c37ac1
RBR
125 textStorage.addAttributes(
126 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 3))
77d0155b
RBR
127 }
128
129 matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range)
130
131 for match in matches {
e2c37ac1
RBR
132 textStorage.addAttributes(
133 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
134 textStorage.addAttributes(
135 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
136 textStorage.addAttributes(
137 [.foregroundColor: NSColor.syntax.symbol], range: match.range(at: 3))
138 textStorage.addAttributes(
139 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 4))
77d0155b
RBR
140 }
141
142 matches = blockerRegex.matches(in: textStorage.string, options: [], range: range)
143
144 for match in matches {
e2c37ac1
RBR
145 textStorage.addAttributes(
146 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
147 textStorage.addAttributes(
148 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
fdb4633d 149 }
e2c37ac1 150
fdb4633d
RBR
151 matches = noteRegex.matches(in: textStorage.string, options: [], range: range)
152
153 for match in matches {
e2c37ac1
RBR
154 textStorage.addAttributes(
155 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
156 textStorage.addAttributes(
157 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
158 textStorage.addAttributes(
159 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 3))
77d0155b
RBR
160 }
161
162 matches = stageRegex.matches(in: textStorage.string, options: [], range: range)
163
164 for match in matches {
e2c37ac1
RBR
165 textStorage.addAttributes(
166 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
167 textStorage.addAttributes(
168 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
169 }
170
171 matches = groupRegex.matches(in: textStorage.string, options: [], range: range)
172
173 for match in matches {
174 textStorage.addAttributes(
175 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
176 textStorage.addAttributes(
177 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
77d0155b
RBR
178 }
179 }
180}
181
5e8ff485
RBR
182struct MapTextEditor: NSViewControllerRepresentable {
183
e2c37ac1 184 @Binding var document: MapDocument
fdb4633d 185 var onChange: () -> Void = {}
5e8ff485
RBR
186
187 func makeNSViewController(
188 context: NSViewControllerRepresentableContext<MapTextEditor>
189 ) -> MapTextEditorController {
e2c37ac1 190 return MapTextEditorController(document: $document, onChange: onChange)
5e8ff485
RBR
191 }
192
193 func updateNSViewController(
194 _ nsViewController: MapTextEditorController,
195 context: NSViewControllerRepresentableContext<MapTextEditor>
fdb4633d 196 ) {}
5e8ff485 197}